今天,我們將實作健身房管理系統中一個非常關鍵的功能——會員管理。我們會構建會員列表頁面和會員詳情頁面,並且學習如何使用 TanStack Table 和 React Hook Form 這些功能強大的工具來實現管理功能。這些功能將幫助我們有效地管理健身房的會員資料。
首先,我們會建立一個會員列表頁面,使用 TanStack Table 來處理表格的排序和分頁功能,這樣可以更方便地管理大量的會員資料。
在 src/pages/Members.tsx
中:
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { useTable, useSortBy, usePagination } from '@tanstack/react-table';
import axios from 'axios';
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
interface Member {
id: number;
name: string;
email: string;
joinDate: string;
membershipType: string;
}
const fetchMembers = async (): Promise<Member[]> => {
const { data } = await axios.get<Member[]>('/api/members');
return data;
};
const Members: React.FC = () => {
const { data: members, isLoading, error } = useQuery(['members'], fetchMembers);
const columns = React.useMemo(
() => [
{
Header: '姓名',
accessor: 'name',
},
{
Header: 'Email',
accessor: 'email',
},
{
Header: '加入日期',
accessor: 'joinDate',
},
{
Header: '會員類型',
accessor: 'membershipType',
},
],
[]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
nextPage,
previousPage,
canNextPage,
canPreviousPage,
prepareRow,
} = useTable(
{ columns, data: members || [] },
useSortBy,
usePagination
);
if (isLoading) return <div>載入中...</div>;
if (error) return <div>發生錯誤:{(error as Error).message}</div>;
return (
<div>
<h1 className="text-2xl font-bold mb-4">會員列表</h1>
<Table {...getTableProps()}>
<TableHeader>
{headerGroups.map(headerGroup => (
<TableRow {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<TableHead {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
<span>
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</span>
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row);
return (
<TableRow {...row.getRowProps()}>
{row.cells.map(cell => (
<TableCell {...cell.getCellProps()}>{cell.render('Cell')}</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
<div className="mt-4">
<Button onClick={() => previousPage()} disabled={!canPreviousPage}>
上一頁
</Button>
<Button onClick={() => nextPage()} disabled={!canNextPage}>
下一頁
</Button>
</div>
</div>
);
};
export default Members;
接下來,我們會實作會員詳情頁面,這個頁面允許管理員查看並編輯特定會員的詳細資料。我們會使用 React Hook Form 來處理表單提交和表單驗證。
在 src/pages/MemberDetail.tsx
中:
import React from 'react';
import { useParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import axios from 'axios';
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card"
interface Member {
id: number;
name: string;
email: string;
joinDate: string;
membershipType: string;
}
const fetchMember = async (id: string): Promise<Member> => {
const { data } = await axios.get<Member>(`/api/members/${id}`);
return data;
};
const updateMember = async (member: Member): Promise<Member> => {
const { data } = await axios.put<Member>(`/api/members/${member.id}`, member);
return data;
};
const MemberDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const queryClient = useQueryClient();
const { data: member, isLoading, error } = useQuery(['member', id], () => fetchMember(id!));
const { register, handleSubmit, formState: { errors } } = useForm<Member>();
const mutation = useMutation(updateMember, {
onSuccess: () => {
queryClient.invalidateQueries(['member', id]);
},
});
const onSubmit = (data: Member) => {
mutation.mutate(data);
};
if (isLoading) return <div>載入中...</div>;
if (error) return <div>發生錯誤:{(error as Error).message}</div>;
return (
<Card>
<CardHeader>
<CardTitle>會員詳情</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-4">
<Input {...register('name', { required: '請輸入姓名' })} defaultValue={member?.name} placeholder="姓名" />
{errors.name && <p className="text-red-500">{errors.name.message}</p>}
<Input {...register('email', { required: '請輸入 Email' })} defaultValue={member?.email} placeholder="Email" />
{errors.email && <p className="text-red-500">{errors.email.message}</p>}
<Input {...register('joinDate')} defaultValue={member?.joinDate} placeholder="加入日期" />
<Input {...register('membershipType')} defaultValue={member?.membershipType} placeholder="會員類型" />
</div>
<Button type="submit" className="mt-4">更新會員資料</Button>
</form>
</CardContent>
</Card>
);
};
export default MemberDetail;
最後,我們需要將這些新頁面整合到我們的路由中。在 src/App.tsx
中:
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import Dashboard from './pages/Dashboard';
import Members from './pages/Members';
import MemberDetail from './pages/MemberDetail';
import Login from './pages/Login';
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<MainLayout />}>
<Route index element={<Dashboard />} />
<Route path="members" element={<Members />} />
<Route path="members/:id" element={<MemberDetail />} />
</Route>
</Routes>
</Router>
);
}
export default App;
今天我們完成了 Gym Pro 系統的會員管理功能,包括以下內容: